4me-sdk 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,15 +13,18 @@ require 'sdk4me/client/multipart'
13
13
  require 'sdk4me/client/attachments'
14
14
 
15
15
  # cherry-pick some core extensions from active support
16
- require 'active_support/core_ext/module/aliasing.rb'
16
+ require 'active_support/core_ext/module/aliasing'
17
17
  require 'active_support/core_ext/object/blank'
18
- require 'active_support/core_ext/object/try.rb'
18
+ require 'active_support/core_ext/object/try'
19
19
  require 'active_support/core_ext/hash/indifferent_access'
20
20
 
21
21
  module Sdk4me
22
22
  class Client
23
23
  MAX_PAGE_SIZE = 100
24
- DEFAULT_HEADER = {'Content-Type' => 'application/json'}
24
+ DEFAULT_HEADER = {
25
+ 'Content-Type' => 'application/json',
26
+ 'User-Agent' => "4me-sdk-ruby/#{Sdk4me::Client::VERSION}"
27
+ }.freeze
25
28
 
26
29
  # Create a new 4me SDK Client
27
30
  #
@@ -37,13 +40,14 @@ module Sdk4me
37
40
  #
38
41
  # All options available:
39
42
  # - logger: The Ruby Logger instance, default: Logger.new(STDOUT)
40
- # - host: The 4me API host, default: 'https://api.4me.com'
41
- # - api_version: The 4me API version, default: 'v1'
43
+ # - host: The 4me REST API host, default: 'https://api.4me.com'
44
+ # - api_version: The 4me REST API version, default: 'v1'
42
45
  # - access_token: *required* The 4me access token
43
46
  # - account: Specify a different (trusted) account to work with
44
47
  # @see https://developer.4me.com/v1/#multiple-accounts
45
48
  # - source: The Source used when creating new records
46
49
  # @see https://developer.4me.com/v1/general/source/
50
+ # - user_agent: The User-Agent header of each request
47
51
  #
48
52
  # - max_retry_time: maximum nr of seconds to wait for server to respond (default = 5400 = 1.5 hours)
49
53
  # the sleep time between retries starts at 2 seconds and doubles after each retry
@@ -59,16 +63,16 @@ module Sdk4me
59
63
  # - proxy_password: Proxy password
60
64
  def initialize(options = {})
61
65
  @options = Sdk4me.configuration.current.merge(options)
62
- [:host, :api_version].each do |required_option|
63
- raise ::Sdk4me::Exception.new("Missing required configuration option #{required_option}") if option(required_option).blank?
66
+ %i[host api_version].each do |required_option|
67
+ raise ::Sdk4me::Exception, "Missing required configuration option #{required_option}" if option(required_option).blank?
64
68
  end
65
69
  @logger = @options[:logger]
66
70
  @ssl, @domain, @port = ssl_domain_port_path(option(:host))
67
71
  unless option(:access_token).present?
68
72
  if option(:api_token).blank?
69
- raise ::Sdk4me::Exception.new("Missing required configuration option access_token")
73
+ raise ::Sdk4me::Exception, 'Missing required configuration option access_token'
70
74
  else
71
- @logger.info('Use of api_token is deprecated, consider switching to access_token instead.')
75
+ @logger.info('DEPRECATED: Use of api_token is deprecated, switch to using access_token instead. -- https://developer.4me.com/v1/#authentication')
72
76
  end
73
77
  end
74
78
  @ssl_verify_none = options[:ssl_verify_none]
@@ -84,15 +88,16 @@ module Sdk4me
84
88
  # Returns total nr of resources yielded (for logging)
85
89
  def each(path, params = {}, header = {}, &block)
86
90
  # retrieve the resources using the max page size (least nr of API calls)
87
- next_path = expand_path(path, {per_page: MAX_PAGE_SIZE, page: 1}.merge(params))
91
+ next_path = expand_path(path, { per_page: MAX_PAGE_SIZE, page: 1 }.merge(params))
88
92
  size = 0
89
93
  while next_path
90
94
  # retrieve the records (with retry and optionally wait for rate-limit)
91
95
  response = get(next_path, {}, header)
92
96
  # raise exception in case the response is invalid
93
- raise ::Sdk4me::Exception.new(response.message) unless response.valid?
97
+ raise ::Sdk4me::Exception, response.message unless response.valid?
98
+
94
99
  # yield the resources
95
- response.json.each{ |resource| yield resource }
100
+ response.json.each(&block)
96
101
  size += response.json.size
97
102
  # go to the next page
98
103
  next_path = response.pagination_relative_link(:next)
@@ -111,14 +116,14 @@ module Sdk4me
111
116
  end
112
117
 
113
118
  # send HTTPS PATCH request and return instance of Sdk4me::Response
114
- def put(path, data = {}, header = {})
115
- _send(json_request(Net::HTTP::Patch, path, data, header))
119
+ def patch(path, data = {}, header = {})
120
+ _send(json_request(Net::HTTP::Patch, path, data, expand_header(header)))
116
121
  end
117
- alias_method :patch, :put
122
+ alias put patch
118
123
 
119
124
  # send HTTPS POST request and return instance of Sdk4me::Response
120
125
  def post(path, data = {}, header = {})
121
- _send(json_request(Net::HTTP::Post, path, data, header))
126
+ _send(json_request(Net::HTTP::Post, path, data, expand_header(header)))
122
127
  end
123
128
 
124
129
  # upload a CSV file to import
@@ -135,17 +140,19 @@ module Sdk4me
135
140
  @logger.info { "Import file '#{csv.path}' successfully uploaded with token '#{response[:token]}'." } if response.valid?
136
141
 
137
142
  if block_until_completed
138
- raise ::Sdk4me::UploadFailed.new("Failed to queue #{type} import. #{response.message}") unless response.valid?
143
+ raise ::Sdk4me::UploadFailed, "Failed to queue #{type} import. #{response.message}" unless response.valid?
144
+
139
145
  token = response[:token]
140
- while true
146
+ loop do
141
147
  response = get("/import/#{token}")
142
148
  unless response.valid?
143
149
  sleep(5)
144
150
  response = get("/import/#{token}") # single retry to recover from a network error
145
- raise ::Sdk4me::Exception.new("Unable to monitor progress for #{type} import. #{response.message}") unless response.valid?
151
+ raise ::Sdk4me::Exception, "Unable to monitor progress for #{type} import. #{response.message}" unless response.valid?
146
152
  end
147
153
  # wait 30 seconds while the response is OK and import is still busy
148
- break unless ['queued', 'processing'].include?(response[:state])
154
+ break unless %w[queued processing].include?(response[:state])
155
+
149
156
  @logger.debug { "Import of '#{csv.path}' is #{response[:state]}. Checking again in 30 seconds." }
150
157
  sleep(30)
151
158
  end
@@ -161,7 +168,7 @@ module Sdk4me
161
168
  # @param locale: Required for translations export
162
169
  # @raise Sdk4me::Exception in case the export progress could not be monitored
163
170
  def export(types, from = nil, block_until_completed = false, locale = nil)
164
- data = {type: [types].flatten.join(',')}
171
+ data = { type: [types].flatten.join(',') }
165
172
  data[:from] = from unless from.blank?
166
173
  data[:locale] = locale unless locale.blank?
167
174
  response = post('/export', data)
@@ -174,17 +181,19 @@ module Sdk4me
174
181
  end
175
182
 
176
183
  if block_until_completed
177
- raise ::Sdk4me::UploadFailed.new("Failed to queue '#{data[:type]}' export. #{response.message}") unless response.valid?
184
+ raise ::Sdk4me::UploadFailed, "Failed to queue '#{data[:type]}' export. #{response.message}" unless response.valid?
185
+
178
186
  token = response[:token]
179
- while true
187
+ loop do
180
188
  response = get("/export/#{token}")
181
189
  unless response.valid?
182
190
  sleep(5)
183
191
  response = get("/export/#{token}") # single retry to recover from a network error
184
- raise ::Sdk4me::Exception.new("Unable to monitor progress for '#{data[:type]}' export. #{response.message}") unless response.valid?
192
+ raise ::Sdk4me::Exception, "Unable to monitor progress for '#{data[:type]}' export. #{response.message}" unless response.valid?
185
193
  end
186
194
  # wait 30 seconds while the response is OK and export is still busy
187
- break unless ['queued', 'processing'].include?(response[:state])
195
+ break unless %w[queued processing].include?(response[:state])
196
+
188
197
  @logger.debug { "Export of '#{data[:type]}' is #{response[:state]}. Checking again in 30 seconds." }
189
198
  sleep(30)
190
199
  end
@@ -193,41 +202,37 @@ module Sdk4me
193
202
  response
194
203
  end
195
204
 
196
- def logger
197
- @logger
198
- end
205
+ attr_reader :logger
199
206
 
200
207
  private
201
208
 
202
209
  # create a request (place data in body if the request becomes too large)
203
- def json_request(request_class, path, data = {}, header = {})
204
- Sdk4me::Attachments.new(self).upload_attachments!(path, data)
205
- request = request_class.new(expand_path(path), expand_header(header))
210
+ def json_request(request_class, path, data, header)
211
+ Sdk4me::Attachments.new(self, path).upload_attachments!(data)
212
+ request = request_class.new(expand_path(path), header)
206
213
  body = {}
207
- data.each{ |k,v| body[k.to_s] = typecast(v, false) }
214
+ data.each { |k, v| body[k.to_s] = typecast(v, false) }
208
215
  request.body = body.to_json
209
216
  request
210
217
  end
211
218
 
212
- URI_ESCAPE_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
213
219
  def uri_escape(value)
214
- URI.escape(value, URI_ESCAPE_PATTERN).gsub('.', '%2E')
220
+ URI.encode_www_form_component(value).gsub('+', '%20').gsub('.', '%2E')
215
221
  end
216
222
 
217
223
  # Expand the given header with the default header
218
- def expand_header(header = {})
219
- header = DEFAULT_HEADER.merge(header)
224
+ def expand_header(headers = {})
225
+ header = DEFAULT_HEADER.dup
220
226
  header['X-4me-Account'] = option(:account) if option(:account)
221
227
  if option(:access_token).present?
222
- header['AUTHORIZATION'] = 'Bearer ' + option(:access_token)
228
+ header['AUTHORIZATION'] = "Bearer #{option(:access_token)}"
223
229
  else
224
230
  token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
225
- header['AUTHORIZATION'] = 'Basic ' + [token_and_password].pack('m*').gsub(/\s/, '')
226
- end
227
- if option(:source)
228
- header['X-4me-Source'] = option(:source)
229
- header['HTTP_USER_AGENT'] = option(:source)
231
+ header['AUTHORIZATION'] = "Basic #{[token_and_password].pack('m*').gsub(/\s/, '')}"
230
232
  end
233
+ header['X-4me-Source'] = option(:source) if option(:source)
234
+ header['User-Agent'] = option(:user_agent) if option(:user_agent)
235
+ header.merge!(headers)
231
236
  header
232
237
  end
233
238
 
@@ -238,8 +243,8 @@ module Sdk4me
238
243
  # fields: ['id', 'created_at', 'sourceID']
239
244
  def expand_path(path, params = {})
240
245
  path = path.dup
241
- path = "/#{path}" unless path =~ /^\// # make sure path starts with /
242
- path = "/#{option(:api_version)}#{path}" unless path =~ /^\/v[\d.]+\// # preprend api version
246
+ path = "/#{path}" unless path =~ %r{^/} # make sure path starts with /
247
+ path = "/#{option(:api_version)}#{path}" unless path =~ %r{^/v[\d.]+/} # preprend api version
243
248
  params.each do |key, value|
244
249
  path << (path['?'] ? '&' : '?')
245
250
  path << expand_param(key, value)
@@ -258,18 +263,20 @@ module Sdk4me
258
263
  # Parameter value typecasting
259
264
  def typecast(value, escape = true)
260
265
  case value.class.name.to_sym
261
- when :NilClass then ''
262
- when :String then escape ? uri_escape(value) : value
263
- when :TrueClass then 'true'
264
- when :FalseClass then 'false'
265
- when :DateTime then datetime = value.new_offset(0).iso8601; escape ? uri_escape(datetime) : datetime
266
- when :Date then value.strftime("%Y-%m-%d")
267
- when :Time then value.strftime("%H:%M")
266
+ when :NilClass then ''
267
+ when :String then escape ? uri_escape(value) : value
268
+ when :TrueClass then 'true'
269
+ when :FalseClass then 'false'
270
+ when :DateTime
271
+ datetime = value.new_offset(0).iso8601
272
+ escape ? uri_escape(datetime) : datetime
273
+ when :Date then value.strftime('%Y-%m-%d')
274
+ when :Time then value.strftime('%H:%M')
268
275
  # do not convert arrays in put/post requests as squashing arrays is only used in filtering
269
- when :Array then escape ? value.map{ |v| typecast(v, escape) }.join(',') : value
276
+ when :Array then escape ? value.map { |v| typecast(v, escape) }.join(',') : value
270
277
  # TODO: temporary for special constructions to update contact details, see Request #1444166
271
- when :Hash then escape ? value.to_s : value
272
- else escape ? value.to_json : value.to_s
278
+ when :Hash then escape ? value.to_s : value
279
+ else escape ? value.to_json : value.to_s
273
280
  end
274
281
  end
275
282
 
@@ -277,27 +284,27 @@ module Sdk4me
277
284
  # Guaranteed to return a Response, thought it may be +empty?+
278
285
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
279
286
  @logger.debug { "Sending #{request.method} request to #{domain}:#{port}#{request.path}" }
280
- _response = begin
287
+ response = begin
281
288
  http_with_proxy = option(:proxy_host).blank? ? Net::HTTP : Net::HTTP::Proxy(option(:proxy_host), option(:proxy_port), option(:proxy_user), option(:proxy_password))
282
289
  http = http_with_proxy.new(domain, port)
283
290
  http.read_timeout = option(:read_timeout)
284
291
  http.use_ssl = ssl
285
292
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
286
- http.start{ |_http| _http.request(request) }
287
- rescue ::Exception => e
293
+ http.start { |transport| transport.request(request) }
294
+ rescue StandardError => e
288
295
  Struct.new(:body, :message, :code, :header).new(nil, "No Response from Server - #{e.message} for '#{domain}:#{port}#{request.path}'", 500, {})
289
296
  end
290
- response = Sdk4me::Response.new(request, _response)
291
- if response.valid?
292
- @logger.debug { "Response:\n#{JSON.pretty_generate(response.json)}" }
293
- elsif response.raw.body =~ /^\s*<\?xml/i
294
- @logger.debug { "XML response:\n#{response.raw.body}" }
295
- elsif '303' == response.raw.code.to_s
296
- @logger.debug { "Redirect: #{response.raw.header['Location']}" }
297
+ resp = Sdk4me::Response.new(request, response)
298
+ if resp.valid?
299
+ @logger.debug { "Response:\n#{JSON.pretty_generate(resp.json)}" }
300
+ elsif resp.raw.body =~ /^\s*<\?xml/i
301
+ @logger.debug { "XML response:\n#{resp.raw.body}" }
302
+ elsif resp.raw.code.to_s == '303'
303
+ @logger.debug { "Redirect: #{resp.raw.header['Location']}" }
297
304
  else
298
- @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{response.message}" }
305
+ @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{resp.message}" }
299
306
  end
300
- response
307
+ resp
301
308
  end
302
309
 
303
310
  # parse the given URI to [domain, port, ssl, path]
@@ -306,65 +313,70 @@ module Sdk4me
306
313
  ssl = uri.scheme == 'https'
307
314
  [ssl, uri.host, uri.port, uri.path]
308
315
  end
309
-
310
316
  end
311
317
 
312
318
  module SendWithRateLimitBlock
313
319
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
314
320
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
315
- return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time) > 0
321
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
322
+
316
323
  now = nil
317
324
  timed_out = false
318
- begin
319
- _response = super(request, domain, port, ssl)
325
+ response = nil
326
+ loop do
327
+ response = super(request, domain, port, ssl)
320
328
  now ||= Time.now
321
- if _response.throttled?
329
+ if response.throttled?
322
330
  # if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
323
- retry_after = _response.retry_after == 0 ? 300 : [_response.retry_after, 2].max
331
+ retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
324
332
  if (Time.now - now + retry_after) < option(:max_throttle_time)
325
- @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{_response.message}" }
333
+ @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{response.message}" }
326
334
  sleep(retry_after)
327
335
  else
328
336
  timed_out = true
329
337
  end
330
338
  end
331
- end while _response.throttled? && !timed_out
332
- _response
339
+ break unless response.throttled? && !timed_out
340
+ end
341
+ response
333
342
  end
334
343
  end
335
- Client.send(:prepend, SendWithRateLimitBlock)
344
+ Client.prepend SendWithRateLimitBlock
336
345
 
337
346
  module SendWithRetries
338
347
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
339
348
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
340
- return super(request, domain, port, ssl) unless option(:max_retry_time) > 0
349
+ return super(request, domain, port, ssl) unless option(:max_retry_time).positive?
350
+
341
351
  retries = 0
342
352
  sleep_time = 1
343
353
  now = nil
344
354
  timed_out = false
345
- begin
346
- _response = super(request, domain, port, ssl)
355
+ response = nil
356
+ loop do
357
+ response = super(request, domain, port, ssl)
347
358
  now ||= Time.now
348
- if _response.failure?
359
+ if response.failure?
349
360
  sleep_time *= 2
350
361
  if (Time.now - now + sleep_time) < option(:max_retry_time)
351
- @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" }
362
+ @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{response.message}" }
352
363
  sleep(sleep_time)
353
364
  else
354
365
  timed_out = true
355
366
  end
356
367
  end
357
- end while _response.failure? && !timed_out
358
- _response
368
+ break unless response.failure? && !timed_out
369
+ end
370
+ response
359
371
  end
360
372
  end
361
- Client.send(:prepend, SendWithRetries)
373
+ Client.prepend SendWithRetries
362
374
  end
363
375
 
364
376
  # HTTPS with certificate bundle
365
377
  module Net
366
378
  class HTTP
367
- alias_method :original_use_ssl=, :use_ssl=
379
+ alias original_use_ssl= use_ssl=
368
380
 
369
381
  def use_ssl=(flag)
370
382
  self.ca_file = File.expand_path(Sdk4me.configuration.current[:ca_file], __FILE__) if flag
@@ -373,4 +385,3 @@ module Net
373
385
  end
374
386
  end
375
387
  end
376
-
@@ -1,152 +1,143 @@
1
1
  module Sdk4me
2
2
  class Attachments
3
+ S3_PROVIDER = 's3'.freeze
4
+ FILENAME_TEMPLATE = '${filename}'.freeze
3
5
 
4
- AWS_PROVIDER = 'aws'
5
- FILENAME_TEMPLATE = '${filename}'
6
-
7
- def initialize(client)
6
+ def initialize(client, path)
8
7
  @client = client
8
+ @path = path
9
9
  end
10
10
 
11
- # upload the attachments and return the data with the uploaded attachment info
12
- # Two flavours available
13
- # * data[:attachments]
14
- # * data[:note] containing text with '[attachment:/tmp/images/green_fuzz.jpg]'
15
- def upload_attachments!(path, data)
16
- upload_options = {
17
- raise_exceptions: !!data.delete(:attachments_exception),
18
- attachments_field: attachments_field(path),
19
- }
20
- uploaded_attachments = upload_normal_attachments!(path, data, upload_options)
21
- uploaded_attachments += upload_inline_attachments!(path, data, upload_options)
22
- # jsonify the attachments, if any were uploaded
23
- data[upload_options[:attachments_field]] = uploaded_attachments.compact.to_json if uploaded_attachments.compact.any?
24
- end
25
-
26
- private
27
-
28
- # upload the attachments in :attachments to 4me and return the data with the uploaded attachment info
29
- def upload_normal_attachments!(path, data, upload_options)
30
- attachments = [data.delete(:attachments)].flatten.compact
31
- return [] if attachments.empty?
32
-
33
- upload_options[:storage] ||= storage(path, upload_options[:raise_exceptions])
34
- return [] unless upload_options[:storage]
35
-
36
- attachments.map do |attachment|
37
- upload_attachment(upload_options[:storage], attachment, upload_options[:raise_exceptions])
11
+ # Upload attachments and replace the data inline with the uploaded
12
+ # attachments info.
13
+ #
14
+ # To upload field attachments:
15
+ # * data[:note_attachments] = ['/tmp/test.doc', '/tmp/test.log']
16
+ #
17
+ # To upload inline images:
18
+ # * data[:note] containing text referring to inline images in
19
+ # data[:note_attachments] by their array index, with the index being
20
+ # zero-based. Text can only refer to inline images in its own
21
+ # attachments collection. For example:
22
+ #
23
+ # data = {
24
+ # note: "Hello [note_attachments: 0] and [note_attachments: 1]",
25
+ # note_attachments: ['/tmp/jip.png', '/tmp/janneke.png'],
26
+ # ...
27
+ # }
28
+ #
29
+ # After calling this method the data that will be posted to update the
30
+ # 4me record would look similar to:
31
+ #
32
+ # data = {
33
+ # note: "Hello ![](storage/abc/adjhajdhjaadf.png) and ![](storage/abc/fskdhakjfkjdssdf.png])",
34
+ # note_attachments: [
35
+ # { key: 'storage/abc/adjhajdhjaadf.png', filesize: 12345, inline: true },
36
+ # { key: 'storage/abc/fskdhakjfkjdssdf.png'], filesize: 98765, inline: true }
37
+ # ],
38
+ # ...
39
+ # }
40
+ def upload_attachments!(data)
41
+ # Field attachments
42
+ field_attachments = []
43
+ data.each do |field, value|
44
+ next unless field.to_s.end_with?('_attachments')
45
+ next unless value.is_a?(Enumerable) && value.any?
46
+
47
+ value.map! { |attachment| upload_attachment(attachment) }.compact!
48
+ field_attachments << field if value.any?
38
49
  end
39
- end
40
50
 
41
- INLINE_ATTACHMENT_REGEXP = /\[attachment:([^\]]+)\]/.freeze
42
- # upload any '[attachment:/tmp/images/green_fuzz.jpg]' in :note text field to 4me as inline attachment and add the s3 key to the text
43
- def upload_inline_attachments!(path, data, upload_options)
44
- text_field = upload_options[:attachments_field].to_s.gsub('_attachments', '').to_sym
45
- return [] unless (data[text_field] || '') =~ INLINE_ATTACHMENT_REGEXP
46
-
47
- upload_options[:storage] ||= storage(path, upload_options[:raise_exceptions])
48
- return [] unless upload_options[:storage]
49
-
50
- attachments = []
51
- data[text_field] = data[text_field].gsub(INLINE_ATTACHMENT_REGEXP) do |full_match|
52
- attachment_details = upload_attachment(upload_options[:storage], $~[1], upload_options[:raise_exceptions])
53
- if attachment_details
54
- attachments << attachment_details.merge(inline: true)
55
- "![](#{attachment_details[:key]})" # magic markdown for inline attachments
56
- else
57
- full_match
51
+ # Rich text inline attachments
52
+ field_attachments.each do |field_attachment|
53
+ field = field_attachment.to_s.sub(/_attachments$/, '')
54
+ value = data[field.to_sym] || data[field]
55
+ next unless value.is_a?(String)
56
+
57
+ value.gsub!(/\[#{field_attachment}:\s?(\d+)\]/) do |match|
58
+ idx = Regexp.last_match(1).to_i
59
+ attachment = data[field_attachment][idx]
60
+ if attachment
61
+ attachment[:inline] = true
62
+ "![](#{attachment[:key]})" # magic markdown for inline attachments
63
+ else
64
+ match
65
+ end
58
66
  end
59
67
  end
60
- attachments
61
68
  end
62
69
 
63
- def storage(path, raise_exceptions)
64
- # retrieve the upload configuration for this record from 4me
65
- storage = @client.get(path =~ /\d+$/ ? path : "#{path}/new", {attachment_upload_token: true}, @client.send(:expand_header))[:storage_upload]
66
- report_error("Attachments not allowed for #{path}", raise_exceptions) unless storage
67
- storage
68
- end
70
+ private
69
71
 
70
- def attachments_field(path)
71
- case path
72
- when /cis/, /contracts/, /flsas/, /service_instances/, /slas/
73
- :remarks_attachments
74
- when /service_offerings/
75
- :summary_attachments
76
- else
77
- :note_attachments
78
- end
72
+ def raise_error(message)
73
+ @client.logger.error { message }
74
+ raise Sdk4me::UploadFailed, message
79
75
  end
80
76
 
81
- def report_error(message, raise_exceptions)
82
- if raise_exceptions
83
- raise Sdk4me::UploadFailed.new(message)
84
- else
85
- @client.logger.error{ message }
86
- end
77
+ def storage
78
+ @storage ||= @client.get('/attachments/storage').json.with_indifferent_access
87
79
  end
88
80
 
89
- # upload a single attachment and return the data for the note_attachments
90
- # returns nil and provides an error in case the attachment upload failed
91
- def upload_attachment(storage, attachment, raise_exceptions)
92
- begin
93
- # attachment is already a file or we need to open the file from disk
94
- unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
95
- raise "file does not exist: #{attachment}" unless File.exists?(attachment)
96
- attachment = File.open(attachment, 'rb')
97
- end
81
+ # Upload a single attachment and return the data that should be submitted
82
+ # back to 4me. Returns nil and provides an error in case the attachment
83
+ # upload failed.
84
+ def upload_attachment(attachment)
85
+ return nil unless attachment
86
+
87
+ provider = storage[:provider]
88
+ raise 'No provider found' unless provider
98
89
 
99
- # there are two different upload methods: AWS S3 and 4me local storage
100
- key_template = "#{storage[:upload_path]}#{FILENAME_TEMPLATE}"
101
- key = key_template.gsub(FILENAME_TEMPLATE, File.basename(attachment.path))
102
- upload_method = storage[:provider] == AWS_PROVIDER ? :aws_upload : :upload_to_4me
103
- send(upload_method, storage, key_template, key, attachment)
104
-
105
- # return the values for the note_attachments param
106
- {key: key, filesize: File.size(attachment.path)}
107
- rescue ::Exception => e
108
- report_error("Attachment upload failed: #{e.message}", raise_exceptions)
109
- nil
90
+ # attachment is already a file or we need to open the file from disk
91
+ unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
92
+ raise "file does not exist: #{attachment}" unless File.exist?(attachment)
93
+
94
+ attachment = File.open(attachment, 'rb')
110
95
  end
96
+
97
+ key_template = storage[provider][:key]
98
+ key = key_template.sub(FILENAME_TEMPLATE, File.basename(attachment.path))
99
+
100
+ key = if provider == S3_PROVIDER
101
+ upload_to_s3(key, attachment)
102
+ else
103
+ upload_to_4me_local(key, attachment)
104
+ end
105
+
106
+ # return the values for the attachments param
107
+ { key: key, filesize: File.size(attachment.path) }
108
+ rescue StandardError => e
109
+ raise_error("Attachment upload failed: #{e.message}")
111
110
  end
112
111
 
113
- def aws_upload(aws, key_template, key, attachment)
114
- # upload the file to AWS
115
- response = send_file(aws[:upload_uri], {
116
- :'x-amz-server-side-encryption' => 'AES256',
117
- key: key_template,
118
- AWSAccessKeyId: aws[:access_key],
119
- acl: 'private',
120
- signature: aws[:signature],
121
- success_action_status: 201,
122
- policy: aws[:policy],
123
- file: attachment # file must be last
124
- })
112
+ # Upload the file to AWS S3 storage
113
+ def upload_to_s3(key, attachment)
114
+ uri = storage[:upload_uri]
115
+ response = send_file(uri, storage[:s3].merge({ file: attachment }))
116
+
125
117
  # this is a bit of a hack, but Amazon S3 returns only XML :(
126
- xml = response.raw.body || ''
127
- error = xml[/<Error>.*<Message>(.*)<\/Message>.*<\/Error>/, 1]
128
- raise "AWS upload to #{aws[:upload_uri]} for #{key} failed: #{error}" if error
118
+ xml = response.body || ''
119
+ error = xml[%r{<Error>.*<Message>(.*)</Message>.*</Error>}, 1]
120
+ raise "AWS S3 upload to #{uri} for #{key} failed: #{error}" if error
129
121
 
130
- # inform 4me of the successful upload
131
- response = @client.get(aws[:success_url].split('/').last, {key: key}, @client.send(:expand_header))
132
- raise "4me confirmation #{aws[:success_url].split('/').last} for #{key} failed: #{response.message}" unless response.valid?
122
+ xml[%r{<Key>(.*)</Key>}, 1]
133
123
  end
134
124
 
135
- # upload the file directly to 4me
136
- def upload_to_4me(storage, key_template, key, attachment)
137
- uri = storage[:upload_uri] =~ /\/v1/ ? storage[:upload_uri] : storage[:upload_uri].gsub('/attachments', '/v1/attachments')
138
- response = send_file(uri, {file: attachment, key: key_template}, @client.send(:expand_header))
139
- raise "4me upload to #{storage[:upload_uri]} for #{key} failed: #{response.message}" unless response.valid?
125
+ # Upload the file directly to 4me local storage
126
+ def upload_to_4me_local(key, attachment)
127
+ uri = storage[:upload_uri]
128
+ response = send_file(uri, storage[:local].merge({ file: attachment }), @client.send(:expand_header))
129
+ raise "4me upload to #{uri} for #{key} failed: #{response.message}" unless response.valid?
130
+
131
+ JSON.parse(response.body)['key']
140
132
  end
141
133
 
142
134
  def send_file(uri, params, basic_auth_header = {})
143
- params = {:'Content-Type' => MIME::Types.type_for(params[:key])[0] || MIME::Types["application/octet-stream"][0]}.merge(params)
135
+ params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
144
136
  data, header = Sdk4me::Multipart::Post.prepare_query(params)
145
137
  ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
146
138
  request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
147
139
  request.body = data
148
140
  @client.send(:_send, request, domain, port, ssl)
149
141
  end
150
-
151
142
  end
152
143
  end